#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/sctp.h>
#include <arpa/inet.h>
#include <cstdlib>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <string>
#include <iostream>
#include <stdexcept>
#include <sstream>
#include <pthread.h>
#include <signal.h>

#include "association_collection.h"
#include "circular_buffer.h"
#include "defines.h"
#include "sctp_one_to_many_connection.h"

#define BUFFER_SIZE 10

int portNumber;
SctpOneToManyConnection connection;

// Creates a timestamp on the format "[11:12:13]", containing the current
// time, and writes it to the timestamp field of the given message.
void writeTimeStamp(struct ChatMessage* message)
{
    time_t rawTime;
    struct tm * currentTime;

    // Create a time stamp
    time(&rawTime);
    currentTime = localtime(&rawTime);

    // Write time stamp to message structure
    strftime (message->timestamp, sizeof(message->timestamp),
            "[%H:%M:%S]", currentTime);
}

// Writes username, timestamp and the given message string to the message
// structure.
void createServerMessage(ChatMessage& message, char const * const messageString)
{
    strncpy(message.username, "Server", sizeof(message.username));
    strncpy(message.message, messageString, sizeof(message.message));

    // Make sure the message is null terminated:
    message.message[MSG_SIZE-1] = '\0';

    writeTimeStamp(&message);
}

// Sends three ASCII images to the client. Each image is sent in a separate
// message on a separate stream.
void handleArtCommand(SctpOneToOneConnection& adminConnection)
{
    std::stringstream image1, image2, image3;
    ChatMessage message1, message2, message3;

    image1 << std::endl <<
"    *************************************"     << std::endl <<
"    *   ___     ____  _________  _____  *"     << std::endl <<
"    *  /   \\   /    \\     |     /     \\ *"  << std::endl <<
"    *  |      |           |     |     | *"     << std::endl <<
"    *   \\_   |            |     |_____/ *"    << std::endl <<
"    *      \\ |            |     |       *"    << std::endl <<
"    *       | |           |     |       *"     << std::endl <<
"    *  \\___/   \\____/     |     |       *"   << std::endl <<
"    *                                   *"     << std::endl <<
"    *************************************"     << std::endl;


    image2                      << std::endl <<
"        AA"                    << std::endl <<
"       oo *"                   << std::endl <<
"       //\\\\ "                << std::endl <<
"      00  \\\\ "               << std::endl <<
"           \\\\ "              << std::endl <<
"            \\\\ "             << std::endl <<
"             \\\\        *"    << std::endl <<
"              \\\\______/"     << std::endl <<
"              /       \\ "     << std::endl <<
"              | ______ |"      << std::endl <<
"              ||      ||"      << std::endl <<
"              ||      ||"      << std::endl <<
"              &&      &&"      << std::endl;

    image3                      << std::endl <<
"          _____ "              << std::endl <<
"         o/ o o \\o"           << std::endl <<
"          | ..  |"             << std::endl <<
"           \\_-_/\\_______S"   << std::endl <<
"             /          \\ "   << std::endl <<
"             | _________ |"    << std::endl <<
"             ||         ||"    << std::endl <<
"            /_|        /_|"    << std::endl;

    // Create three messages containing the images:
    createServerMessage(message1, image1.str().c_str());
    createServerMessage(message2, image2.str().c_str());
    createServerMessage(message3, image3.str().c_str());

    // Send the messages on different streams:
    adminConnection.send(&message1, sizeof(message1), 0);
    adminConnection.send(&message2, sizeof(message2), 1);
    adminConnection.send(&message3, sizeof(message3), 2);
}

// Sets the allowed inactivity time for clients.
void handleAutocloseTimeCommand(SctpOneToOneConnection& adminConnection)
{
    ChatMessage message;

    // Create instruction message:
    createServerMessage(message, "Enter new autoclose time value (seconds):");

    // Send message:
    adminConnection.send(&message, sizeof(message));

    // Get the new value for autoclose:
    adminConnection.receive(&message, sizeof(message));

    // Set the autoclose time:
    int seconds = atoi(message.message);
    connection.setAutoCloseTime(seconds);
}

// Sends an error message to the client, saying that the received command
// was not recognized.
void handleUnknownCommand(SctpOneToOneConnection& adminConnection)
{
    ChatMessage message;

    // Create error message:
    createServerMessage(message, "Unknown command."
            " Remember to prefix the command with /");

    // Send message:
    adminConnection.send(&message, sizeof(message));
}

// Creates a list of all the addresses currently bound by the one-to-many
// and one-to-one connections, and sends the list to the client.
void handleListBoundCommand(SctpOneToOneConnection& adminConnection)
{
    ChatMessage message;
    std::stringstream ss;
    std::vector<Address> addresses;
    int addressCount;

    // Send message with the addresses bound by the one-to-many connection:
    connection.getBoundAddresses(addresses);

    // Create the message
    ss << "The following IP addresses are currently bound by the "
       << "server: " << std::endl;

    // Add the dotted decimal representation of all addresses to the stream:
    addressCount = addresses.size();

    for (int i = 0; i < addressCount; i++)
    {
        ss << addresses[i].getDottedDecimalRepresentation() << " / "
           << addresses[i].getHostName() << std::endl;
    }

    // Create a message from the stream:
    // Observe that there is a risk of that the message does not fit.
    createServerMessage(message, ss.str().c_str());

    adminConnection.send(&message, sizeof(message));


    // Send a message with the addresses bound by the one-to-one connection:

    // Clear the addresses and stringstream from the last message data:
    addresses.clear();
    adminConnection.getBoundAddresses(addresses);
    ss.str("");

    // Create the message
    ss << "The following IP addresses are currently bound by the "
       << "admin connection: " << std::endl;

    // Add the dotted decimal representation of all addresses to the stream:
    addressCount = addresses.size();

    for (int i = 0; i < addressCount; i++)
    {
        ss << addresses[i].getDottedDecimalRepresentation() << " / "
           << addresses[i].getHostName() << std::endl;
    }

    // Create a message from the stream:
    // Observe that there is a risk of that the message does not fit.
    createServerMessage(message, ss.str().c_str());

    adminConnection.send(&message, sizeof(message));
}

// Creates a list of all the addresses currently available at the server
// host, and sends this list to the client.
void handleListAllCommand(SctpOneToOneConnection& adminConnection)
{
    ChatMessage message;
    std::stringstream ss;
    std::vector<Address> addresses;
    int addressCount;

    // Get all available addresses:
    // Call with dummy port since only the IP / host name is of interest.
    Address::getAllAvailable(addresses, 0);

    // Create the message
    ss << "The following IP addresses are currently available to the "
       << "server: " << std::endl;

    // Add the dotted decimal representation of all addresses to the stream:
    addressCount = addresses.size();

    for (int i = 0; i < addressCount; i++)
    {
        ss << addresses[i].getDottedDecimalRepresentation() << " / "
           << addresses[i].getHostName() << std::endl;
    }

    // Create a message from the stream:
    // Observe that there is a risk of that the message does not fit.
    createServerMessage(message, ss.str().c_str());

    adminConnection.send(&message, sizeof(message));
}

// Prompts the client for an IP-address and adds that address to the binding
// of the one-to-many connection.
void handleBindCommand(SctpOneToOneConnection& adminConnection)
{
    ChatMessage message;
    std::vector<Address> addresses;

    // Create instructions message:
    createServerMessage(message, "Enter the IP address to bind:");

    // Send instructions to client:
    adminConnection.send(&message, sizeof(message));

    // Receive IP address to bind:
    adminConnection.receive(&message, sizeof(message));

    // Create an address instance from the received IP and add it to the
    // vector required by bind(...). The port number must be the same as the
    // other addresses so use the global portNumber variable.
    addresses.push_back(Address(message.message, portNumber));

    // Bind the specified address:
    connection.bind(addresses);
}

// Prompts the client for an IP-address and removes that address from the
// binding of the one-to-many connection and the one-to-one connection.
void handleUnbindCommand(SctpOneToOneConnection& adminConnection)
{
    ChatMessage message;
    std::vector<Address> addresses;

    // Create instructions message:
    createServerMessage(message, "Enter the IP address to unbind:");

    // Send instructions to client:
    adminConnection.send(&message, sizeof(message));

    // Receive IP address to unbind:
    adminConnection.receive(&message, sizeof(message));

    // Create an address instance from the received IP and add it to the
    // vector required by bind(...). Use the global variable portNumber to
    // set the port number to the same value as the already bound addresses.
    addresses.push_back(Address(message.message, portNumber));

    // Unbind the specified address:
    connection.unbind(addresses);
    adminConnection.unbind(addresses);
}

// Peels off a one-to-one connection from the one-to-many connection to handle
// an admin session. Reads and handles admin commands until the client quits.
void* handleAdmin(void * associationId)
{
    bool hasReceivedQuitCommand = false;
    ChatMessage message;
    size_t messageStructSize = sizeof(message);
    sctp_assoc_t* assocId;

    // Peel off the association to its own one-to-one connection:
    assocId = (sctp_assoc_t*)associationId;
    SctpOneToOneConnection adminConnection = connection.peelOff(*assocId);

    // The association id variable was dynamically allocated in the main thread,
    // delete the allocated memory:
    delete assocId;

    // Send initial info message:
    createServerMessage(message, "You have been logged in as administrator.\n"
            "The available commands are:\n"
            "bind, unbind, listbound, listall, autoclosetime, art, quit");

    adminConnection.send(&message, messageStructSize);

    try
    {
        // Loop receiving commands until the quit command is received:
        while(!hasReceivedQuitCommand)
        {
            // Receive command from client:
            adminConnection.receive(&message, messageStructSize);

            std::cout << "Received admin command: "
                      << message.message << std::endl;

            // Call the appropriate handler:
            if (strcmp(message.message, "/autoclosetime") == 0)
                handleAutocloseTimeCommand(adminConnection);
            else if (strcmp(message.message, "/listbound") == 0)
                handleListBoundCommand(adminConnection);
            else if (strcmp(message.message, "/listall") == 0)
                handleListAllCommand(adminConnection);
            else if (strcmp(message.message, "/bind") == 0)
                handleBindCommand(adminConnection);
            else if (strcmp(message.message, "/unbind") == 0)
                handleUnbindCommand(adminConnection);
            else if (strcmp(message.message, "/art") == 0)
                handleArtCommand(adminConnection);
            else if (strcmp(message.message, "/quit") == 0)
                hasReceivedQuitCommand = true; // Exit the loop
            else
                handleUnknownCommand(adminConnection);
        }
    }
    // If the client crashes or is killed during the admin communication
    // the error must be caught so that the whole server does not crash.
    // An exception in this thread would crash the whole process and thus the
    // whole server.
    catch (std::runtime_error error)
    {
        std::cerr << "Unexpected error in admin communication: "
                  << error.what() << std::endl;
    }

    adminConnection.close();
    pthread_exit(NULL);
}

// Reads and handles commands given via the console. The handled commands are
// /listbound, /bind and /unbind.
void* handleUserInput(void* dummy)
{
    std::string command, argument;
    std::vector<Address> addresses;

    while(1)
    {
        addresses.clear();

        // Read  command from console
        getline(std::cin, command);

        if (command == "/bind")
        {
            // Read IP-address:
            getline(std::cin, argument);

            // Bind address:
            addresses.push_back(Address(argument.c_str(), portNumber));
            connection.bind(addresses);
        }
        else if (command == "/unbind")
        {
            // Read IP-address:
            getline(std::cin, argument);

            // Unbind IP-address:
            addresses.push_back(Address(argument.c_str(), portNumber));
            connection.unbind(addresses);
        }
        else if (command == "/listbound")
        {
            // Print bound addresses:
            int addressCount;

            connection.getBoundAddresses(addresses);

            // Create the message
            std::cout << "The following IP addresses are currently bound by the"
                      << std::endl << " server: " << std::endl;

            // Add the dotted decimal representation of all addresses to the stream:
            addressCount = addresses.size();

            for (int i = 0; i < addressCount; i++)
            {
                std::cout << addresses[i].getDottedDecimalRepresentation()
                          << " / " << addresses[i].getHostName() << std::endl;
            }
        }
        else
        {
            std::cout << "Unknown command" << std::endl;
            continue;
        }
    }
}

// Chat server. Contains a master loop that receives messages from clients
// and sends them to the other clients if they are chat messages or handles
// them if they are commands. Spawns a separate thread for reading commands
// given via the console.
int main(int argc, char* argv[])
{
    struct ChatMessage message;
    pthread_t tempThreadHandle;
    pthread_attr_t detachedAttr;
    CircularBuffer<ChatMessage> messageBuffer(BUFFER_SIZE);
    CircularBufferConstIterator<ChatMessage> messageIterator =
        messageBuffer.begin();
    AssociationCollection associations;
    AssociationCollectionConstIterator assocIterator = associations.begin();
    SctpReceiveResult result;
    std::vector<Address> addresses;

    // Check that enough command line argument have been given by the user:
    if (argc < 2)
    {
        std::cerr << "usage: " << argv[0] << " <port> <address 1>"
                  << " ... <address n>" << std::endl;
        exit(-1);
    }

    // Read port number from the first command line argument:
    portNumber = atoi(argv[1]);

    // Create an instance of Address for each IP given by the user, and
    // create a listening connection that binds to those addresses:
    if (argc > 2)
    {
        // Create a vector of all the IP addresses given as parameters:
        for (int i = 2; i < argc; i++)
            addresses.push_back(Address(argv[i], portNumber));

        connection =
            SctpOneToManyConnection::createListeningConnection(addresses);
    }
    // If the user did not explicitly specify which addresses to use,
    // bind all available addresses:
    else
    {
        connection =
            SctpOneToManyConnection::createListeningConnection(portNumber);
    }

    // Ignore the SIGPIPE signal, to avoid crashing if an admin connection
    // is unexpectantly closed:

    // Initialize POSIX threads
    pthread_attr_init(&detachedAttr);
    pthread_attr_setdetachstate(&detachedAttr, PTHREAD_CREATE_DETACHED);

    // Spawn thread for reading user input:
    pthread_create(&tempThreadHandle, &detachedAttr, &handleUserInput, NULL);

    // Main message handling loop:
    // Runs until SIGINT/SIGKILL/etc.
    while(1)
    {
        // Receive message
        result = connection.receiveMessage(&message, sizeof(message));

        if (result.getMessageSize() < 0)
        {
            std::cout << "Error while receiving message: " << errno
                      << std::endl;
        }

        // Convert client address to dotted decimal form
        std::cout << "Received message from "
                  << result.getPeerAddress().getDottedDecimalRepresentation()
                  << ", The message was: " << message.message << std::endl
                  << "The association id is: " << result.getAssociationId()
                  << std::endl;

        // If the message was the administrator login command, peel off
        // the association into a one-to-one socket and handle it separately:
        if (strcmp(message.message, "/admin") == 0)
        {
            sctp_assoc_t* assocId = new sctp_assoc_t;
            *assocId = result.getAssociationId();
            pthread_create(&tempThreadHandle, &detachedAttr, &handleAdmin,
                    assocId);
            continue;
        }
        else if (strcmp(message.message, "/quit") == 0)
        {
            // Rewrite the message to a info message that announces that
            // the user has left:

            // Use username to create info message:
            // Use snprintf instead of sprintf to avoid buffer overflow:
            snprintf(message.message, sizeof(message.message),
                    "%s has left the chat.", message.username);

            // Since it now is an info message, use "Server" as username:
            strncpy(message.username, "Server", sizeof(message.username));
        }
        else if (strcmp(message.message, "/getbuffer") == 0)
        {
            // Output status message:
            std::cout << "Sending message buffer to association "
                      << result.getAssociationId() << std::endl;

            // For each message in the buffer, send to the association that
            // sent the /getbuffer command:
            for (messageIterator = messageBuffer.begin();
                    !messageIterator.isAtEnd(); messageIterator++)
            {
                // Only send buffer messages that actually contain a message:
                if (messageIterator.data().message[0] != '\0')
                {
                    // Send the message to the client:
                    connection.send(&messageIterator.data(),
                            sizeof(struct ChatMessage),
                            result.getAssociationId());
                }
            }

            continue;
        }

        // Add receive time to the message:
        writeTimeStamp(&message);

        // Add the message to the message buffer:
        messageBuffer.pushBack(message);

        // Output status message:
        std::cout << "Sending received message to all clients." << std::endl;

        // Send the received message to all clients:
        connection.sendToAll(&message, sizeof(struct ChatMessage));
    }

    // Clean up
    connection.close();

    return 0;
}
